﻿<!DOCTYPE html>
<html>
<head>
  <title>Movable, Copyable, Deletable Ports</title>
  <!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
  <meta name="description" content="Nodes with selectable, movable, copyable, and deletable ports." />
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script src="../release/go.js"></script>
  <script src="../assets/js/goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
  <script id="code">
    function init() {
      if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
      var $ = go.GraphObject.make;

      myDiagram =
        $(go.Diagram, "myDiagramDiv",
          {
            "undoManager.isEnabled": true,
            // don't allow links within a group
            "linkingTool.linkValidation": differentGroups,
            "relinkingTool.linkValidation": differentGroups,
            mouseDrop: function(e) {
              // when the selection is dropped in the diagram's background,
              // and it includes any "port"s, cancel the drop
              if (myDiagram.selection.any(selectionIncludesPorts)) {
                myDiagram.currentTool.doCancel();
              }
            }
          });

      function differentGroups(fromnode, fromport, tonode, toport) {
        return fromnode.containingGroup !== tonode.containingGroup;
      }

      function selectionIncludesPorts(n) {
        return n.containingGroup !== null && !myDiagram.selection.has(n.containingGroup);
      }

      var UnselectedBrush = "lightgray";  // item appearance, if not "selected"
      var SelectedBrush = "dodgerblue";   // item appearance, if "selected"

      myDiagram.nodeTemplate =
        $(go.Node, "Auto",
          { selectionAdorned: false },
          {
            mouseDrop: function(e, n) {
              // when the selection is entirely ports and is dropped onto a Group, transfer membership
              if (n.containingGroup !== null && myDiagram.selection.all(selectionIncludesPorts)) {
                myDiagram.selection.each(function(p) { p.containingGroup = n.containingGroup; });
              } else {
                myDiagram.currentTool.doCancel();
              }
            }
          },
          $(go.Shape,
            {
              name: "SHAPE",
              fill: UnselectedBrush, stroke: "gray",
              geometryString: "F1 m 0,0 l 5,0 1,4 -1,4 -5,0 1,-4 -1,-4 z",
              spot1: new go.Spot(0, 0, 5, 1),  // keep the text inside the shape
              spot2: new go.Spot(1, 1, -5, 0),
              // some port-related properties
              portId: "",
              toSpot: go.Spot.Left,
              toLinkable: false,
              fromSpot: go.Spot.Right,
              fromLinkable: false,
              cursor: "pointer"
            },
            new go.Binding("fill", "isSelected", function(s) { return s ? SelectedBrush : UnselectedBrush; }).ofObject(),
            new go.Binding("toLinkable", "_in"),
            new go.Binding("fromLinkable", "_in", function(b) { return !b; })),
          $(go.TextBlock,
            new go.Binding("text", "name"))
        );

      myDiagram.groupTemplate =
        $(go.Group, "Auto",
          {
            selectionAdorned: false,
            locationSpot: go.Spot.Center, locationObjectName: "ICON"
          },
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
          {
            mouseDrop: function(e, g) {
              // when the selection is entirely ports and is dropped onto a Group, transfer membership
              if (myDiagram.selection.all(selectionIncludesPorts)) {
                myDiagram.selection.each(function(p) { p.containingGroup = g; });
              } else {
                myDiagram.currentTool.doCancel();
              }
            },
            layout: new InputOutputGroupLayout()
          },
          $(go.Shape, "RoundedRectangle",
            { stroke: "gray", strokeWidth: 2, fill: "transparent" },
            new go.Binding("stroke", "isSelected", function(b) { return b ? SelectedBrush : UnselectedBrush; }).ofObject()),
          $(go.Panel, "Vertical",
            { margin: 6 },
            $(go.TextBlock,
              new go.Binding("text", "name"),
              { alignment: go.Spot.Left }),
            $(go.Panel, "Spot",
              { name: "ICON", height: 60 },  // an initial height; size will be set by InputOutputGroupLayout
              $(go.Shape,
                { fill: null, stroke: null, stretch: go.GraphObject.Fill }),
              $(go.Picture, "images/60x90.png",
                { width: 30, height: 45 })
            )
          )
        );

      myDiagram.linkTemplate =
        $(go.Link,
          { routing: go.Link.Orthogonal, corner: 10, toShortLength: -3 },
          { relinkableFrom: true, relinkableTo: true },
          $(go.Shape, { stroke: "gray", strokeWidth: 2.5 })
        );

      load();  // initialize myDiagram's model from the TextArea
    }

    function findPortNode(g, name, input) {
      for (var it = g.memberParts; it.next();) {
        var n = it.value;
        if (!(n instanceof go.Node)) continue;
        if (n.data.name === name && n.data._in === input) return n;
      }
      return null;
    }

    // Transform the given data to the data structures needed internally.
    // For each data object in the "ins" Array of the node data, add a "port" Node to the Group.
    // For each data object in the "outs" Array, add a "port" Node to the Group.
    // For each link data, convert the "from" and "fromPort" information to the actual "port" Node,
    // and then the same for "to" and "toPort".
    // The internal model uses property names starting with "_" to avoid having Model.toJson() write them out.
    function setupDiagram(nodes, links) {
      var model = new go.GraphLinksModel();
      model.linkFromKeyProperty = "_f";
      model.linkToKeyProperty = "_t";
      model.nodeIsGroupProperty = "_isg";
      model.nodeGroupKeyProperty = "_g";

      // first create all of the nodes, implemented as Groups
      for (var i = 0; i < nodes.length; i++) {
        var nodedata = nodes[i];
        nodedata._isg = true;
      }
      model.addNodeDataCollection(nodes);
      // now each node data will have a unique key, if not already specified

      // then create all of the ports, implemented as Nodes that are members of those Groups
      for (var i = 0; i < nodes.length; i++) {
        var nodedata = nodes[i];
        if (Array.isArray(nodedata.ins)) {
          for (var j = 0; j < nodedata.ins.length; j++) {
            var portdata = nodedata.ins[j];
            portdata._in = true;
            portdata._g = nodedata.key;
          }
          model.addNodeDataCollection(nodedata.ins);
          nodedata.ins = undefined;
        }
        if (Array.isArray(nodedata.outs)) {
          for (var j = 0; j < nodedata.outs.length; j++) {
            var portdata = nodedata.outs[j];
            portdata._in = false;
            portdata._g = nodedata.key;
          }
          model.addNodeDataCollection(nodedata.outs);
          nodedata.outs = undefined;
        }
      }

      myDiagram.model = model;
      // now Groups and Nodes exist, so can find the Node corresponding to a node's port

      // finally process all of the links, to account for ports actually being member nodes
      for (var i = 0; i < links.length; i++) {
        var linkdata = links[i];
        var fromNode = myDiagram.findNodeForKey(linkdata.from);
        var toNode = myDiagram.findNodeForKey(linkdata.to);
        if (fromNode !== null && toNode !== null) {
          // look in the Group for a "port" Node with the right name and directionality
          var fromPortNode = findPortNode(fromNode, linkdata.fromPort, false);
          var toPortNode = findPortNode(toNode, linkdata.toPort, true);
          if (fromPortNode !== null && toPortNode !== null) {
            linkdata._f = fromPortNode.data.key;
            linkdata._t = toPortNode.data.key;
            linkdata.from = linkdata.fromPort = linkdata.to = linkdata.toPort = undefined;
          }
        }
      }
      model.addLinkDataCollection(links);
    }

    function save() {
      // can't just call myDiagram.model.toJson() -- need to transform to external format
      var m = new go.GraphLinksModel();
      m.linkFromPortIdProperty = "fromPort";
      m.linkToPortIdProperty = "toPort";
      var arr = myDiagram.model.nodeDataArray;
      myDiagram.nodes.each(function(g) {
        if (g instanceof go.Group) {
          g.data.ins = undefined;
          g.data.outs = undefined;
          m.addNodeData(g.data);
        }
      });
      myDiagram.nodes.each(function(n) {
        if (!(n instanceof go.Group)) {
          var gd = n.containingGroup.data;
          var a = n.data._in ? gd.ins : gd.outs;
          if (!a) {
            a = [];
            if (n.data._in) gd.ins = a; else gd.outs = a;
          }
          a.push(n.data);
        }
      });
      myDiagram.links.each(function(l) {
        l.data.from = l.fromNode.containingGroup.data.key;
        l.data.fromPort = l.fromNode.data.name;
        l.data.to = l.toNode.containingGroup.data.key;
        l.data.toPort = l.toNode.data.name;
        m.addLinkData(l.data);
      });
      document.getElementById("mySavedModel").value = m.toJson();
      myDiagram.isModified = false;
    }

    function load() {
      var m = go.Model.fromJson(document.getElementById("mySavedModel").value);
      setupDiagram(m.nodeDataArray, m.linkDataArray);
    }


    // The Group.layout, for arranging the "port" Nodes within the Group
    function InputOutputGroupLayout() {
      go.Layout.call(this);
    }
    go.Diagram.inherit(InputOutputGroupLayout, go.Layout);

    InputOutputGroupLayout.prototype.doLayout = function(coll) {
      coll = this.collectParts(coll);

      var portSpacing = 2;
      var iconAreaWidth = 60;

      // compute the counts and areas of the inputs and the outputs
      var left = 0;
      var leftwidth = 0;  // max
      var leftheight = 0; // total
      var right = 0;
      var rightwidth = 0;  // max
      var rightheight = 0; // total
      coll.each(function(n) {
        if (n instanceof go.Link) return;  // ignore Links
        if (n.data._in) {
          left++;
          leftwidth = Math.max(leftwidth, n.actualBounds.width);
          leftheight += n.actualBounds.height;
        } else {
          right++;
          rightwidth = Math.max(rightwidth, n.actualBounds.width);
          rightheight += n.actualBounds.height;
        }
      });
      if (left > 0) leftheight += portSpacing * (left - 1);
      if (right > 0) rightheight += portSpacing * (right - 1);

      var loc = new go.Point(0, 0);
      if (this.group !== null && this.group.location.isReal()) loc = this.group.location;

      // first lay out the left side, the inputs
      var y = loc.y - leftheight / 2;
      coll.each(function(n) {
        if (n instanceof go.Link) return;  // ignore Links
        if (!n.data._in) return;  // ignore outputs
        n.position = new go.Point(loc.x - iconAreaWidth / 2 - leftwidth, y);
        y += n.actualBounds.height + portSpacing;
      });

      // now the right side, the outputs
      y = loc.y - rightheight / 2;
      coll.each(function(n) {
        if (n instanceof go.Link) return;  // ignore Links
        if (n.data._in) return;  // ignore inputs
        n.position = new go.Point(loc.x + iconAreaWidth / 2 + rightwidth - n.actualBounds.width, y);
        y += n.actualBounds.height + portSpacing;
      });

      // then position the group and size its icon area
      if (this.group !== null) {
        // position the group so that its ICON is in the middle, between the "ports"
        this.group.location = loc;
        // size the ICON so that it's wide enough to overlap the "ports" and tall enough to hold all of the "ports"
        var icon = this.group.findObject("ICON");
        if (icon !== null) icon.desiredSize = new go.Size(iconAreaWidth + leftwidth / 2 + rightwidth / 2, Math.max(leftheight, rightheight) + 10);
      }
    };
  </script>
</head>
<body onload="init()">
<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <p>
    To allow ports to be selected, dragged, copied, and deleted, they are implemented as Nodes.
    That means the nodes have to be implemented as Groups.
    The user can delete selected ports.
    The user cannot drop a port onto the background, but only onto a node.
  </p>
  <p>
    There is a custom Layout used by such Group nodes, <code>InputOutputGroupLayout</code>,
    to line up the input ports on the left side and the output ports on the right side.
  </p>
  <button id="SaveButton" onclick="save()">Save</button>
  <button onclick="load()">Load</button>
  The transformed model data (not the actual myDiagram.model):
  <textarea id="mySavedModel" style="width:100%;height:300px">
    { "class": "go.GraphLinksModel",
    "linkFromPortIdProperty": "fromPort",
    "linkToPortIdProperty": "toPort",
    "nodeDataArray": [
    {"key":1, "name":"Server", "ins":[ {"name":"s1", "key":-3},{"name":"s2", "key":-4} ], "outs":[ {"name":"o1", "key":-5} ], "loc":"-80 -80"},
    {"key":2, "name":"Other", "ins":[ {"name":"s1", "key":-6},{"name":"s2", "key":-7} ], "outs":[ {"name":"o1", "key":-8} ], "loc":"80 80"}
    ],
    "linkDataArray": [
    {"from":1, "fromPort":"o1", "to":2, "toPort":"s2"}
    ]
    }
  </textarea>
</div>
</body>
</html>
